﻿using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Data.Services.Client;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using System.Web.Security;
using Microsoft.WindowsAzure;
using Microsoft.WindowsAzure.ServiceRuntime;
using Microsoft.WindowsAzure.StorageClient;

namespace jp110311.StorageModels
{
    [CLSCompliant(false)]
    public class MembershipEntity : TableServiceEntity
    {
        public MembershipEntity() : base()
        {
        }

        public MembershipEntity( string identityName ) : base( identityName.Substring(0,7), identityName.Substring(7) )
        {
            IdentityName = identityName;
        }

        public string IdentityName { get; set; }

        public string PasswordHashed { get; set; }

        public string Email { get; set; }

        public string PasswordQuestion { get; set; }

        public string PasswordAnswer { get; set; }

        public bool IsApproved { get; set; }

        public string ProviderUserKey { get; set; }

        public bool IsLockedOut { get; set; }

        public DateTime CreationDate { get; set; }

        public DateTime LastLoginDate { get; set; }

        public DateTime LastActivityDate { get; set; }

        public DateTime LastPasswordChangedDate { get; set; }

        public DateTime LastLockoutDate { get; set; }
    }

    public class AzureTableMembershipProvider : MembershipProvider
    {
        private const string MembershipTableName = "membership";
        private bool _tableCreated;
        private string _accountConnectionStringName;
        private bool _useDevelopmentStorage;
        private int _minRequiredPasswordLength;

        public override void Initialize(string name, NameValueCollection config)
        {
            _useDevelopmentStorage = bool.Parse(config["useDevelopmentStorage"]);
            _accountConnectionStringName = config["storageAccountConnectionStringName"];
            _minRequiredPasswordLength = int.Parse(config["minRequiredPasswordLength"]);

            config.Remove("storageAccountConnectionStringName");
            base.Initialize(name, config);
        }

        /// <summary>
        /// 新しいメンバーシップ ユーザーをデータ ソースに追加します。
        /// </summary>
        /// <returns>
        /// 新しく作成されたユーザーの情報が格納された <see cref="T:System.Web.Security.MembershipUser"/> オブジェクト。
        /// </returns>
        /// <param name="username">新しいユーザーのユーザー名。</param><param name="password">新しいユーザーのパスワード。</param><param name="email">新しいユーザーの電子メール アドレス。</param><param name="passwordQuestion">新しいユーザーのパスワードの質問。</param><param name="passwordAnswer">新しいユーザーのパスワードの解答。</param><param name="isApproved">新しいユーザーを承認するかどうか。</param><param name="providerUserKey">メンバーシップ データ ソースでのユーザーの一意の識別子。</param><param name="status">ユーザーが正常に作成されたかどうかを示す <see cref="T:System.Web.Security.MembershipCreateStatus"/> 列挙値。</param>
        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            Guid innerProviderUserKey;
            if (providerUserKey != null && providerUserKey.GetType()==typeof(Guid))
            {
                innerProviderUserKey = (Guid) providerUserKey;
            }
            else
            {
                innerProviderUserKey = Guid.NewGuid();
            }
            SHA1 sha1 = SHA1.Create();
            string passwordHashed = Convert.ToBase64String( sha1.ComputeHash(Encoding.UTF8.GetBytes(password)) );
            MembershipEntity membershipEntity =
                new MembershipEntity(username)
                {
                    PasswordHashed = passwordHashed,
                    Email = email,
                    PasswordQuestion = passwordQuestion,
                    PasswordAnswer = passwordAnswer,
                    IsApproved = isApproved,
                    ProviderUserKey = innerProviderUserKey.ToString(),
                    CreationDate = DateTime.UtcNow,
                    LastActivityDate = DateTime.UtcNow,
                    LastLockoutDate = DateTime.UtcNow,
                    LastLoginDate = DateTime.UtcNow,
                    LastPasswordChangedDate = DateTime.UtcNow,
                    IsLockedOut = false
                };
            TableServiceContext context = GetDataServiceContext();
            var q = context.CreateQuery<MembershipEntity>(MembershipTableName);
            var existingEntity =
                q.Where(e => e.PartitionKey == membershipEntity.PartitionKey && e.RowKey == membershipEntity.RowKey).SingleOrDefault();
            if (existingEntity != null)
            {
                status = MembershipCreateStatus.DuplicateUserName;
                return null;
            }

            context.AddObject(MembershipTableName, membershipEntity);
            try
            {
                context.SaveChanges();
            }
            catch
            {
                status = MembershipCreateStatus.ProviderError;
                return null;
            }

            status = MembershipCreateStatus.Success;

            return this.GetUser(username, false);
        }

        private TableServiceContext GetDataServiceContext()
        {
            CloudStorageAccount account = GetStorageAccount();
            CloudTableClient client = account.CreateCloudTableClient();
            if (!_tableCreated)
            {
                client.CreateTableIfNotExist(MembershipTableName);
                _tableCreated = true;
            }
            
            var context= client.GetDataServiceContext();
            if (DataServiceContextCreated != null)
            {
                DataServiceContextCreated( this, new DataServiceContextCreatedEventArgs(context));
            }

            context.RetryPolicy = RetryPolicies.Retry(3,TimeSpan.FromMilliseconds(30));
            context.IgnoreResourceNotFoundException = true;
            return context;
        }

        public static event EventHandler<DataServiceContextCreatedEventArgs> DataServiceContextCreated;

        private CloudStorageAccount GetStorageAccount()
        {
            if (!RoleEnvironment.IsAvailable) return CloudStorageAccount.DevelopmentStorageAccount;
            if (_useDevelopmentStorage) return CloudStorageAccount.DevelopmentStorageAccount;
            return CloudStorageAccount.FromConfigurationSetting(_accountConnectionStringName);
        }

        /// <summary>
        /// メンバーシップ ユーザーに対するパスワードの質問と解答の更新要求を処理します。
        /// </summary>
        /// <returns>
        /// パスワードの質問と解答が正常に更新された場合は true。それ以外の場合は false。
        /// </returns>
        /// <param name="username">パスワードの質問と解答を変更するユーザー。</param><param name="password">指定したユーザーのパスワード。</param><param name="newPasswordQuestion">指定したユーザーの新しいパスワード。</param><param name="newPasswordAnswer">指定したユーザーの新しいパスワードの解答。</param>
        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// 指定したメンバーシップ ユーザーのパスワードをデータ ソースから取得します。
        /// </summary>
        /// <returns>
        /// 指定したユーザー名のパスワード。
        /// </returns>
        /// <param name="username">パスワードを取得するユーザー。</param><param name="answer">ユーザーのパスワードの解答。</param>
        public override string GetPassword(string username, string answer)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// メンバーシップ ユーザーに対するパスワードの更新要求を処理します。
        /// </summary>
        /// <returns>
        /// パスワードが正常に更新された場合は true。それ以外の場合は false。
        /// </returns>
        /// <param name="username">パスワードを更新するユーザー。</param><param name="oldPassword">指定したユーザーの現在のパスワード。</param><param name="newPassword">指定したユーザーの新しいパスワード。</param>
        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// ユーザーのパスワードを、自動的に生成された新しいパスワードにリセットします。
        /// </summary>
        /// <returns>
        /// 指定したユーザーの新しいパスワード。
        /// </returns>
        /// <param name="username">パスワードをリセットするユーザー。</param><param name="answer">指定したユーザーのパスワードの解答。</param>
        public override string ResetPassword(string username, string answer)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// データ ソース内のユーザー情報を更新します。
        /// </summary>
        /// <param name="user">更新するユーザーとそのユーザーの更新情報を表す <see cref="T:System.Web.Security.MembershipUser"/> オブジェクト。</param>
        public override void UpdateUser(MembershipUser user)
        {
            if (user.ProviderName != Name) throw new InvalidOperationException("メンバシッププロバイダが異なります");

            TableServiceContext context = GetDataServiceContext();
            MembershipEntity existingEntity = GetMembershipEntity(context, user.UserName);
            if (existingEntity == null)
            {
                throw new InvalidOperationException("更新対象ユーザーが見つかりません");
            }
            existingEntity.CreationDate = user.CreationDate;
            existingEntity.Email = user.Email;
            existingEntity.IsApproved = user.IsApproved;
            existingEntity.IsLockedOut = user.IsLockedOut;
            existingEntity.LastActivityDate = user.LastActivityDate;
            existingEntity.LastLockoutDate = user.LastLockoutDate;
            existingEntity.LastLoginDate = user.LastLoginDate;
            existingEntity.LastPasswordChangedDate = user.LastPasswordChangedDate;
            
            context.UpdateObject(existingEntity);
            context.SaveChanges(SaveChangesOptions.None);
        }

        private MembershipEntity GetMembershipEntity(TableServiceContext context, string userName)
        {
            var q = context.CreateQuery<MembershipEntity>(MembershipTableName);

            string partitionKey = userName.Substring(0, 7);
            string rowKey = userName.Substring(7);
            return q.Where(e => e.PartitionKey == partitionKey && e.RowKey == rowKey).SingleOrDefault();
        }

        /// <summary>
        /// 指定のユーザー名とパスワードがデータ ソースに存在しているかどうかを検証します。
        /// </summary>
        /// <returns>
        /// 指定したユーザー名とパスワードが有効な場合は true。それ以外の場合は false。
        /// </returns>
        /// <param name="username">検証対象のユーザー名。</param><param name="password">指定したユーザーのパスワード。</param>
        public override bool ValidateUser(string username, string password)
        {
            SHA1 sha1 = SHA1.Create();
            string passwordHashed = Convert.ToBase64String( sha1.ComputeHash(Encoding.UTF8.GetBytes(password)) );

            TableServiceContext context = GetDataServiceContext();
            MembershipEntity existingEntity = GetMembershipEntity(context, username );
            if (existingEntity == null) return false;
            if (existingEntity.PasswordHashed != passwordHashed) return false;
            return existingEntity.IsApproved && !existingEntity.IsLockedOut;
        }

        /// <summary>
        /// ロックを解除して、メンバーシップ ユーザーの検証を可能にします。
        /// </summary>
        /// <returns>
        /// メンバーシップ ユーザーのロックが正常に解除された場合は true。それ以外の場合は false。
        /// </returns>
        /// <param name="userName">ロック ステータスを解除するメンバーシップ ユーザー。</param>
        public override bool UnlockUser(string userName)
        {
            TableServiceContext context = GetDataServiceContext();
            MembershipEntity existingEntity = GetMembershipEntity(context, userName);
            existingEntity.IsLockedOut = false;
            context.UpdateObject(existingEntity);
            context.SaveChanges(SaveChangesOptions.None);
            return true;
        }

        /// <summary>
        /// メンバーシップ ユーザーの一意の識別子に基づいて、データ ソースからユーザー情報を取得します。ユーザーの最終利用日時スタンプを更新するオプションも提供されます。
        /// </summary>
        /// <returns>
        /// データ ソースから取得された指定のユーザーの情報が格納された <see cref="T:System.Web.Security.MembershipUser"/> オブジェクト。
        /// </returns>
        /// <param name="providerUserKey">情報を取得するメンバーシップ ユーザーの一意の識別子。</param><param name="userIsOnline">ユーザーの最終利用日時スタンプを更新する場合は true。ユーザーの最終利用日時スタンプを更新しないでユーザー情報を返す場合は false。</param>
        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// データ ソースからユーザーの情報を取得します。ユーザーの最終利用日時スタンプを更新するオプションも提供されます。
        /// </summary>
        /// <returns>
        /// データ ソースから取得された指定のユーザーの情報が格納された <see cref="T:System.Web.Security.MembershipUser"/> オブジェクト。
        /// </returns>
        /// <param name="username">ユーザー情報を取得するユーザーの名前。</param><param name="userIsOnline">ユーザーの最終利用日時スタンプを更新する場合は true。ユーザーの最終利用日時スタンプを更新しないでユーザー情報を返す場合は false。</param>
        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            TableServiceContext context = GetDataServiceContext();
            var membershipEntity = GetMembershipEntity(context, username);
            if (membershipEntity == null) return null;
            return new MembershipUser(this.Name, membershipEntity.IdentityName, membershipEntity.ProviderUserKey, membershipEntity.Email,
                membershipEntity.PasswordQuestion, null, membershipEntity.IsApproved, membershipEntity.IsLockedOut, membershipEntity.CreationDate,
                membershipEntity.LastLoginDate, membershipEntity.LastActivityDate, membershipEntity.LastPasswordChangedDate, membershipEntity.LastLockoutDate);
        }

        /// <summary>
        /// 指定した電子メール アドレスに関連付けられているユーザー名を取得します。
        /// </summary>
        /// <returns>
        /// 指定した電子メール アドレスに関連付けられているユーザー名。一致する電子メール アドレスが見つからない場合、このメソッドは null を返します。
        /// </returns>
        /// <param name="email">検索対象の電子メール アドレス。</param>
        public override string GetUserNameByEmail(string email)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// メンバーシップ データ ソースからユーザーを削除します。
        /// </summary>
        /// <returns>
        /// ユーザーが正常に削除された場合は true。それ以外の場合は false。
        /// </returns>
        /// <param name="username">削除するユーザーの名前。</param><param name="deleteAllRelatedData">データベースからそのユーザーに関連するデータを削除する場合は true。データベース内のそのユーザーに関連するデータをそのままにしておく場合は false。</param>
        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// データ ソース内のすべてのユーザーのページのコレクションを取得します。
        /// </summary>
        /// <returns>
        /// <paramref name="pageIndex"/> で指定されたページから始まる <paramref name="pageSize"/><see cref="T:System.Web.Security.MembershipUser"/> オブジェクトのページを格納している <see cref="T:System.Web.Security.MembershipUserCollection"/>。
        /// </returns>
        /// <param name="pageIndex">取得する結果のページのインデックス。<paramref name="pageIndex"/> が 0 から始まっています。</param><param name="pageSize">取得する結果のページのサイズ。</param><param name="totalRecords">一致したユーザーの総数。</param>
        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// アプリケーションに現在アクセスしているユーザーの数を取得します。
        /// </summary>
        /// <returns>
        /// アプリケーションに現在アクセスしているユーザーの数。
        /// </returns>
        public override int GetNumberOfUsersOnline()
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// ユーザー名が指定内容と一致するメンバーシップ ユーザーのコレクションを取得します。
        /// </summary>
        /// <returns>
        /// <paramref name="pageIndex"/> で指定されたページから始まる <paramref name="pageSize"/><see cref="T:System.Web.Security.MembershipUser"/> オブジェクトのページを格納している <see cref="T:System.Web.Security.MembershipUserCollection"/>。
        /// </returns>
        /// <param name="usernameToMatch">検索するユーザー名。</param><param name="pageIndex">取得する結果のページのインデックス。<paramref name="pageIndex"/> が 0 から始まっています。</param><param name="pageSize">取得する結果のページのサイズ。</param><param name="totalRecords">一致したユーザーの総数。</param>
        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// 電子メール アドレスが指定内容と一致するメンバーシップ ユーザーのコレクションを取得します。
        /// </summary>
        /// <returns>
        /// <paramref name="pageIndex"/> で指定されたページから始まる <paramref name="pageSize"/><see cref="T:System.Web.Security.MembershipUser"/> オブジェクトのページを格納している <see cref="T:System.Web.Security.MembershipUserCollection"/>。
        /// </returns>
        /// <param name="emailToMatch">検索対象の電子メール アドレス。</param><param name="pageIndex">取得する結果のページのインデックス。<paramref name="pageIndex"/> が 0 から始まっています。</param><param name="pageSize">取得する結果のページのサイズ。</param><param name="totalRecords">一致したユーザーの総数。</param>
        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// ユーザーがパスワードを取得できるように、メンバーシップ プロバイダーが構成されているかどうかを示します。
        /// </summary>
        /// <returns>
        /// メンバーシップ プロバイダーがパスワードの取得をサポートしている場合は true。それ以外の場合は false。既定値は false です。
        /// </returns>
        public override bool EnablePasswordRetrieval
        {
            get { throw new NotImplementedException(); }
        }

        /// <summary>
        /// ユーザーがパスワードをリセットできるように、メンバーシップ プロバイダーが構成されているかどうかを示します。
        /// </summary>
        /// <returns>
        /// メンバーシップ プロバイダーがパスワードのリセットをサポートしている場合は true。それ以外の場合は false。既定値は true です。
        /// </returns>
        public override bool EnablePasswordReset
        {
            get { throw new NotImplementedException(); }
        }

        /// <summary>
        /// ユーザーがパスワードをリセットおよび取得する際にパスワードの質問に答えなければならないように、メンバーシップ プロバイダーが構成されているかどうかを示す値を取得します。
        /// </summary>
        /// <returns>
        /// パスワードをリセットまたは取得する際にパスワードの解答を要求する場合は true。それ以外の場合は false。既定値は true です。
        /// </returns>
        public override bool RequiresQuestionAndAnswer
        {
            get { throw new NotImplementedException(); }
        }

        /// <summary>
        /// カスタム メンバーシップ プロバイダーを使用するアプリケーションの名前。
        /// </summary>
        /// <returns>
        /// カスタム メンバーシップ プロバイダーを使用するアプリケーションの名前。
        /// </returns>
        public override string ApplicationName
        {
            get { throw new NotImplementedException(); }
            set { throw new NotImplementedException(); }
        }

        /// <summary>
        /// メンバーシップ ユーザーがロックされるまでの無効なパスワードまたはパスワード解答の指定回数を取得します。
        /// </summary>
        /// <returns>
        /// メンバーシップ ユーザーがロックされるまでの無効なパスワードまたはパスワード解答の指定回数。
        /// </returns>
        public override int MaxInvalidPasswordAttempts
        {
            get { throw new NotImplementedException(); }
        }

        /// <summary>
        /// 無効なパスワードまたはパスワードの解答を最大回数まで指定できる、メンバーシップ ユーザーがロックされるまでの期間を示す分数を取得します。
        /// </summary>
        /// <returns>
        /// 無効なパスワードまたはパスワードの解答を最大回数まで指定できる、メンバーシップ ユーザーがロックされるまでの期間を示す分数。
        /// </returns>
        public override int PasswordAttemptWindow
        {
            get { throw new NotImplementedException(); }
        }

        /// <summary>
        /// メンバーシップ プロバイダーが各ユーザー名に対して一意の電子メール アドレスを要求するかどうかを示す値を取得します。
        /// </summary>
        /// <returns>
        /// メンバーシップ プロバイダーが一意の電子メール アドレスを必要とする場合は true。それ以外の場合は false。既定値は true です。
        /// </returns>
        public override bool RequiresUniqueEmail
        {
            get { throw new NotImplementedException(); }
        }

        /// <summary>
        /// メンバーシップ データ ソースにパスワードを格納する形式を示す値を取得します。
        /// </summary>
        /// <returns>
        /// データ ソースにパスワードを格納する形式を示す <see cref="T:System.Web.Security.MembershipPasswordFormat"/> 値の 1 つ。
        /// </returns>
        public override MembershipPasswordFormat PasswordFormat
        {
            get { throw new NotImplementedException(); }
        }

        /// <summary>
        /// パスワードに最低限必要な長さを取得します。
        /// </summary>
        /// <returns>
        /// パスワードに最低限必要な長さ。
        /// </returns>
        public override int MinRequiredPasswordLength
        {
            get { return _minRequiredPasswordLength; }
        }

        /// <summary>
        /// 有効なパスワードに最低限含むことが必要な特殊文字の数を取得します。
        /// </summary>
        /// <returns>
        /// 有効なパスワードに最低限含むことが必要な特殊文字の数。
        /// </returns>
        public override int MinRequiredNonAlphanumericCharacters
        {
            get { throw new NotImplementedException(); }
        }

        /// <summary>
        /// パスワードの評価に使用される正規表現を取得します。
        /// </summary>
        /// <returns>
        /// パスワードの評価に使用される正規表現。
        /// </returns>
        public override string PasswordStrengthRegularExpression
        {
            get { throw new NotImplementedException(); }
        }
    }

    public class DataServiceContextCreatedEventArgs : EventArgs
    {
        public DataServiceContext Context { get; private set; }
        public DataServiceContextCreatedEventArgs( DataServiceContext context )
        {
            Context = context;
        }
    }
}
